Skip to content

处理搜索交互逻辑#1023

Merged
CodFrm merged 13 commits into
mainfrom
fix/search
Nov 28, 2025
Merged

处理搜索交互逻辑#1023
CodFrm merged 13 commits into
mainfrom
fix/search

Conversation

@CodFrm
Copy link
Copy Markdown
Member

@CodFrm CodFrm commented Nov 25, 2025

概述 Descriptions

@cyfung1031

遇到个很奇怪的问题,看了大半天了,可能得研究arco的实现了: https://github.com/arco-design/arco-design/blob/8960a171b5d26d7ce04dbde4a83db70eb13f95e8/components/Table/thead/column.tsx#L70

删除下面这行代码就不能 直接修改 自动/名字/代码 进行筛选,添加上只能修改一次,改过去再改回来不会重新筛选,但是v1.2.0-beta.1 可以无限次筛选

v1.2.0-beta.1: https://github.com/scriptscat/scriptcat/blob/v1.2.0-beta.1/src/pages/options/routes/ScriptList.tsx#L957

79505ef

Jietu20251125-161724-HD.mp4

变更内容 Changes

截图 Screenshots

@CodFrm CodFrm requested a review from cyfung1031 November 25, 2025 08:18
@CodFrm
Copy link
Copy Markdown
Member Author

CodFrm commented Nov 25, 2025

看arco的设计,是需要点确定(调用confirm)才会进行筛选的,感觉这实际上是个bug

初步猜测是因为修改了对象引用导致的

@cyfung1031
Copy link
Copy Markdown
Collaborator

好的。我会看看
既然旧版本都做得到,新版本不可能不行
要是不行就改回我原本的那个呀。卡片那个兼容反而很容易搞吧

把这个都改掉就应该可以发布 1.2 正式版了

@CodFrm
Copy link
Copy Markdown
Member Author

CodFrm commented Nov 25, 2025

好的。我会看看

既然旧版本都做得到,新版本不可能不行

要是不行就改回我原本的那个呀。卡片那个兼容反而很容易搞吧

把这个都改掉就应该可以发布 1.2 正式版了

现在我已经改好了,但总感觉是运行在bug上

@cyfung1031
Copy link
Copy Markdown
Collaborator

先不要合并。我晚点再找个时间看一下

@cyfung1031
Copy link
Copy Markdown
Collaborator

cyfung1031 commented Nov 26, 2025

改好了

现在有点尴尬了
现在才发现 auto 跟 code 的结果是一模一样。。。。
因为 @name XXXXX 是 code 的一部份。。。。

不过先这样吧


你说的那个奇怪问题
官方文档中, filterKeys只能是 string. 现在只是刚好 object 也可以。
所以有一定程度的奇怪行为是没办法啦
在React的世界,只要碰到「set」的问题,都用 {...} [...] 等方式避免参考重复就对啦

@CodFrm CodFrm added the P0 🚑 需要紧急处理的内容 label Nov 28, 2025
@CodFrm CodFrm requested a review from Copilot November 28, 2025 01:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

本 PR 旨在解决脚本列表搜索过滤功能的交互问题。在之前的版本中,修改搜索类型(自动/名字/代码)时,过滤功能要么无法正常工作,要么只能工作一次。本次修改通过重构搜索过滤逻辑,引入新的 SearchFilter 类来管理搜索状态和缓存,解决了过滤器重复触发和状态不一致的问题。

主要改动:

  • 新增 SearchFilter.ts 类来统一管理搜索过滤逻辑和缓存
  • 重构 ScriptTable 组件的搜索过滤实现,使用表格内置的过滤功能
  • 移除 ref + onFilterDropdownVisibleChange 的手动聚焦方式,改用 autoFocus 属性
  • 优化搜索交互流程,支持搜索类型切换时立即触发过滤

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/pages/options/routes/ScriptList/SearchFilter.ts 新增搜索过滤器类,使用模块级缓存管理搜索结果,提供可扩展的过滤逻辑
src/pages/options/routes/ScriptList/ScriptTable.tsx 重构表格搜索过滤实现,移除 searchRequestsetSearchRequest props,使用内部 SearchFilter 实例处理过滤逻辑
src/pages/options/routes/ScriptList/hooks.tsx 更新 useScriptSearch hook,引入 SearchFilter 类处理搜索请求,优化过滤状态管理
src/pages/options/routes/ScriptList/components.tsx 移除 ScriptSearchFieldReact.memo 优化和自定义比较函数,添加 onChange 回调支持实时搜索,修复搜索类型比较运算符
src/pages/options/routes/ScriptList/index.tsx 传递完整的 scriptList 给表格组件而非预过滤的列表,让表格组件自行处理过滤
src/pages/options/routes/SubscribeList.tsx 移除 inputRefonFilterDropdownVisibleChange,改用 autoFocus 属性实现自动聚焦
src/pages/components/ScriptStorage/index.tsx 移除 inputRefonFilterDropdownVisibleChange,改用 autoFocus 属性实现自动聚焦
src/pages/components/ScriptResource/index.tsx 移除 inputRefonFilterDropdownVisibleChange,改用 autoFocus 属性实现自动聚焦
src/pages/options/routes/utils.tsx 修改 ListHomeRender 组件,为数组项添加 key 属性以消除 React 警告

Comment thread src/pages/options/routes/ScriptList/components.tsx Outdated
Comment on lines +16 to +57
export class SearchFilter {
constructor() {}
requestFilterResult(req: SearchFilterRequest): void {
if (req.keyword === lastKeyword) {
lastReqType = req.type;
this.onResponse(req, lastResponse);
} else {
requestFilterResult({ value: req.keyword }).then((res) => {
lastReqType = req.type;
lastKeyword = req.keyword;
lastResponse = res;
searchFilterCache.clear();
if (res && Array.isArray(res)) {
for (const entry of res) {
searchFilterCache.set(entry.uuid, {
code: entry.code,
name: entry.name,
auto: entry.auto,
});
}
}
this.onResponse(req, res);
});
}
}
onResponse(_req: SearchFilterRequest, _res: SearchFilterResponse): void {
// placeholder
}
checkByUUID(uuid: string): boolean {
const result = searchFilterCache.get(uuid);
if (!result) return false;
switch (lastReqType) {
case "auto":
return result.auto;
case "script_code":
return result.code;
case "name":
return result.name;
default:
return false;
}
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新增的 SearchFilter 类缺少类和方法的文档注释。建议添加 JSDoc 注释来说明:

  1. 类的用途和设计模式(例如使用了模板方法模式)
  2. 各个方法的参数、返回值和副作用
  3. onResponse 方法的回调时机

例如:

/**
 * SearchFilter 类用于处理脚本搜索过滤逻辑
 * 使用模块级缓存来避免重复的搜索请求
 * 子类可以重写 onResponse 方法来处理搜索结果
 */
export class SearchFilter {
  /**
   * 请求过滤结果
   * @param req 搜索请求,包含关键词和搜索类型
   */
  requestFilterResult(req: SearchFilterRequest): void {
    // ...
  }
  
  /**
   * 当搜索结果返回时的回调方法(模板方法)
   * 子类应重写此方法来处理搜索结果
   */
  onResponse(_req: SearchFilterRequest, _res: SearchFilterResponse): void {
    // ...
  }
}

Copilot uses AI. Check for mistakes.
class ScriptTableSearchFilter extends SearchFilter {
filterKeys?: SearchFilterKeyEntry[] = undefined;
setFilterKeys?: SearchFilterKeysSetter = undefined;
defaultValue = { type: "auto", keyword: "" };
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在类中硬编码 defaultValue 作为实例属性可能不够灵活。如果将来需要不同的默认值,需要修改类定义。建议将其作为构造函数参数:

class ScriptTableSearchFilter extends SearchFilter {
  filterKeys?: SearchFilterKeyEntry[] = undefined;
  setFilterKeys?: SearchFilterKeysSetter = undefined;
  constructor(public defaultValue: SearchFilterKeyEntry = { type: "auto", keyword: "" }) {
    super();
  }
  // ...
}
Suggested change
defaultValue = { type: "auto", keyword: "" };
defaultValue: SearchFilterKeyEntry;
constructor(defaultValue: SearchFilterKeyEntry = { type: "auto", keyword: "" }) {
super();
this.defaultValue = defaultValue;
}

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +97
// 表格传入全部脚本,由表格组件处理过滤和排序
scriptList={scriptList}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释说明"表格传入全部脚本,由表格组件处理过滤和排序",但这与卡片视图不一致。卡片视图(第112行)仍然传入 filterScriptList(已过滤的列表)。这种不一致可能会让维护者困惑。

建议:

  1. 统一两种视图的数据传递方式
  2. 或者在注释中明确说明为什么两种视图采用不同的策略

例如:

// 表格传入全部脚本,由表格组件内部的 Arco Table 过滤功能处理过滤
// 卡片视图传入已过滤的列表,因为卡片不支持内置过滤功能
scriptList={scriptList}

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +14
// 静态变数不随重绘重置
let lastReqType: SearchType | undefined = undefined;
let lastKeyword: string = "";
let lastResponse: ScriptCode | undefined = undefined;
const searchFilterCache: Map<string, any> = new Map();
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

模块级静态变量缺少文档说明。这些全局状态变量(lastReqTypelastKeywordlastResponsesearchFilterCache)在多个实例间共享,可能导致意外行为。建议添加注释说明:

  1. 这些变量的用途和生命周期
  2. 为什么它们是模块级别而不是实例级别
  3. 多个 SearchFilter 实例共享这些状态的影响

例如:

// 模块级缓存:所有 SearchFilter 实例共享这些状态
// 用于避免重复的搜索请求,提高性能
// 注意:在多个组件同时使用 SearchFilter 时可能导致状态冲突
let lastReqType: SearchType | undefined = undefined;

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +39
requestFilterResult(req: SearchFilterRequest): void {
if (req.keyword === lastKeyword) {
lastReqType = req.type;
this.onResponse(req, lastResponse);
} else {
requestFilterResult({ value: req.keyword }).then((res) => {
lastReqType = req.type;
lastKeyword = req.keyword;
lastResponse = res;
searchFilterCache.clear();
if (res && Array.isArray(res)) {
for (const entry of res) {
searchFilterCache.set(entry.uuid, {
code: entry.code,
name: entry.name,
auto: entry.auto,
});
}
}
this.onResponse(req, res);
});
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

存在潜在的竞态条件问题。如果用户快速修改搜索条件,多个异步请求可能同时进行,后发的请求可能先返回,导致 lastKeywordlastResponsesearchFilterCache 被旧数据覆盖。建议:

  1. 使用一个请求计数器或时间戳来跟踪最新请求
  2. 忽略过时的响应
    例如:
private requestId = 0;
requestFilterResult(req: SearchFilterRequest): void {
  const currentRequestId = ++this.requestId;
  if (req.keyword === lastKeyword) {
    // ...
  } else {
    requestFilterResult({ value: req.keyword }).then((res) => {
      if (currentRequestId !== this.requestId) return; // 忽略过时响应
      // ...更新状态
    });
  }
}

Copilot uses AI. Check for mistakes.
Comment on lines +553 to +558
class HooksSearchFilter extends SearchFilter {
onResponse(req: SearchFilterRequest, res: SearchFilterResponse) {
setLastFilterQuery({ request: req, response: res });
}
}
const searchFilter = new HooksSearchFilter();
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useScriptSearch hook 内部定义类并在每次重新渲染时创建新实例会导致性能和状态问题:

  1. 每次渲染都会创建新的 HooksSearchFilter 类定义和实例
  2. 在 useEffect 中使用的 searchFilter 实例在每次渲染时都是新的,可能导致闭包问题

建议:

  1. 将类定义移到 hook 外部
  2. 使用 useMemouseRef 来保持实例的稳定性
    例如:
// 在 hook 外部定义
class HooksSearchFilter extends SearchFilter {
  constructor(private setLastFilterQuery: React.Dispatch<React.SetStateAction<...>>) {
    super();
  }
  onResponse(req: SearchFilterRequest, res: SearchFilterResponse) {
    this.setLastFilterQuery({ request: req, response: res });
  }
}

// 在 hook 内部
const searchFilter = useMemo(() => new HooksSearchFilter(setLastFilterQuery), []);

Copilot uses AI. Check for mistakes.
Comment on lines +515 to +518
filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => {
// 重绘时更新React参考
searchFilter.filterKeys = filterKeys;
searchFilter.setFilterKeys = setFilterKeys;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filterDropdown 函数内部更新 searchFilter 实例的属性(filterKeyssetFilterKeys)存在问题。由于 searchFilter 在每次渲染时都是新实例(第474行),而 filterDropdown 是在 useMemo 的依赖项中定义的(第476行的 columns),这可能导致:

  1. 旧的 searchFilter 实例的属性被更新,但该实例已经过时
  2. onFilter 方法(第536行)引用的是过时的 searchFilter 实例

建议使用 useRef 来保持 searchFilter 实例稳定:

const searchFilter = useRef(new ScriptTableSearchFilter()).current;

Copilot uses AI. Check for mistakes.
Comment on lines +565 to +577
searchFilter.requestFilterResult(searchRequest);
}
}, [originMap, scriptList, selectedFilters, tagMap, searchRequest]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchRequest, scriptList]); // scriptList 有改动时也重新查一下最新的结果吧

useEffect(() => {
// 当 filterFuncs 改变时进行 / Filter结果取得时进行
// 按 filterFuncs 过滤一次
let filterList = scriptList.filter((script) => filterFuncs.every((fn) => fn(script)));
if (lastFilterQuery) {
// 再基于关键词过滤一次
filterList = filterList.filter((item) => {
return searchFilter.checkByUUID(item.uuid);
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在 useEffect 中使用 searchFilter 实例存在问题,因为 searchFilter 在每次渲染时都是一个新实例(第558行)。这会导致:

  1. useEffect 的依赖项中应该包含 searchFilter,但注释说"searchFilter 参考固定不变"(第582行)是不正确的
  2. 每次渲染时创建的新实例与上一次渲染时的实例不同,但 useEffect 无法感知到这个变化

建议使用 useRefuseMemo 来保持 searchFilter 实例的稳定性:

const searchFilter = useMemo(() => {
  class HooksSearchFilter extends SearchFilter {
    onResponse(req: SearchFilterRequest, res: SearchFilterResponse) {
      setLastFilterQuery({ request: req, response: res });
    }
  }
  return new HooksSearchFilter();
}, []); // 空依赖数组,实例在组件生命周期内保持不变

Copilot uses AI. Check for mistakes.
Comment thread src/pages/options/routes/ScriptList/ScriptTable.tsx Outdated
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@CodFrm
Copy link
Copy Markdown
Member Author

CodFrm commented Nov 28, 2025

没有看出要使用 export class SearchFilter { 的必要,我看主要是为了防止同样的keywrod重复查询,而且 lastFilterQuery 和 lastReqType 之类的逻辑 跳来跳去的,希望可读性能更好一点

@cyfung1031
Copy link
Copy Markdown
Collaborator

没有看出要使用 export class SearchFilter { 的必要,我看主要是为了防止同样的keywrod重复查询,而且 lastFilterQuery 和 lastReqType 之类的逻辑 跳来跳去的,希望可读性能更好一点

一,兩邊用同一個邏輯取信息做filter, 共通部份多只是顯示方式不同
二,抽出來的class的共通變數不存活於react繪圖元件裡, 元件重繪都不會改變已取得的結果
三,把整個邏輯獨立出來,react部份專注繪畫,抽出來的部份專注邏輯
四,解決你所面對的bug

你要整理也可以。不要整理完又一堆bug一堆效能問題就行

@CodFrm CodFrm merged commit cb38ed0 into main Nov 28, 2025
2 of 3 checks passed
@CodFrm CodFrm deleted the fix/search branch December 3, 2025 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P0 🚑 需要紧急处理的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants